<?php

namespace Xin\HttpClient;

use ArrayAccess;
use Exception;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\TransferStats;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use Xin\Support\Arr;
use Xin\Support\File;
use Xin\Support\Fluent;
use Xin\Support\Str;
use Xin\Support\Traits\Macroable;

/**
 * Class Response
 *
 * @mixin \GuzzleHttp\Psr7\Response
 */
class Response implements ArrayAccess
{

	use Macroable {
		Macroable::__call as macroCall;
	}

	/**
	 * 基础 PSR 响应。
	 *
	 * @var ResponseInterface
	 */
	protected $response;

	/**
	 * 解码的 JSON 或 XML 响应。
	 *
	 * @var array
	 */
	protected $decoded;

	/**
	 * @var BadResponseException
	 */
	protected $exception;

	/**
	 * @var Fluent
	 */
	protected $requestOptions;

	/**
	 * @var HttpClient
	 */
	protected $httpClient;

	/**
	 * @var TransferStats
	 */
	protected $transferStats;

	/**
	 * @var RequestInterface|null
	 */
	protected $request;

	/**
	 * @var SteamIterator
	 */
	protected $steamIterator;

	/**
	 * 创建新的响应实例。
	 *
	 * @param ResponseInterface|null $response
	 * @param Exception|null $exception
	 * @param RequestInterface|null $request
	 * @param array|null $requestOptions
	 * @param HttpClient|null $httpClient
	 * @param TransferStats|null $transferStats
	 */
	public function __construct(
		ResponseInterface $response = null,
		Exception         $exception = null,
		RequestInterface  $request = null,
		array             $requestOptions = null,
		HttpClient        $httpClient = null,
		TransferStats     $transferStats = null,
	)
	{
		$this->response = $response;
		$this->exception = $exception;
		$this->request = $request;
		$this->requestOptions = new Fluent($requestOptions ?: []);
		$this->httpClient = $httpClient;
		$this->transferStats = $transferStats;
	}

	/**
	 * 获取响应的正文。
	 *
	 * @return string
	 */
	public function body()
	{
		return (string)$this->response->getBody();
	}

	/**
	 * 将响应的 JSON 解码。
	 * @param bool $asArray
	 * @return object|array|string|null
	 */
	public function json($asArray = true)
	{
		return json_decode($this->body(), $asArray);
	}

	/**
	 * 将响应的 XML 解码。
	 * @param bool $asArray
	 * @return object|array|null
	 */
	public function xml($asArray = true)
	{
		//将XML转为array,禁止引用外部xml实体
		libxml_disable_entity_loader(true);
		return json_decode(
			json_encode(
				simplexml_load_string($this->body(), 'SimpleXMLElement', LIBXML_NOCDATA)
			),
			$asArray
		);
	}

	/**
	 * 将响应的 JSON 、XML 解码为对象。
	 *
	 * @return object
	 */
	public function object()
	{
		if ($this->isXml()) {
			return $this->xml(false);
		} else {
			return $this->json(false);
		}
	}

	/**
	 * 获取响应的 JSON 或 XML 解码。
	 *
	 * @param string|null $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function decoded($key = null, $default = null)
	{
		if ($this->decoded === null) {
			if ($this->isJson()) {
				$this->decoded = $this->json();
			} elseif ($this->isXml()) {
				$this->decoded = $this->xml();
			}

			if ($this->decoded === null) {
				$this->decoded = [];
			}
		}

		if (is_null($key)) {
			return $this->decoded;
		}

		return Arr::get($this->decoded, $key, $default);
	}

	/**
	 * 获取响应的 JSON 或 XML 解码。
	 *
	 * @param string|null $key
	 * @param mixed $default
	 * @return mixed
	 */
	public function data($key = null, $default = null)
	{
		return $this->decoded($key, $default);
	}

	/**
	 * 仅获取指定的数据
	 * @param array $keys
	 * @return array
	 */
	public function only(array $keys)
	{
		return Arr::only($this->data(), $keys);
	}

	/**
	 * 获取除了指定的数据
	 * @param array $keys
	 * @return array
	 */
	public function except(array $keys)
	{
		return Arr::except($this->data(), $keys);
	}

	/**
	 * 清除响应的 JSON 或 XML 解码正文。
	 *
	 * @return void
	 */
	public function unsetDecoded()
	{
		$this->decoded = null;
	}

	/**
	 * 响应流迭代器
	 * @return SteamIterator
	 */
	public function steamIterator()
	{
		if ($this->steamIterator === null) {
			$this->steamIterator = new SteamIterator($this->response->getBody());
		}

		return $this->steamIterator;
	}

	/**
	 * 设置响应流迭代器回调
	 * @param callable $chunkCallback
	 * @return void
	 */
	public function setOnSteamIteratorChunk(callable $chunkCallback)
	{
		$this->steamIterator()->setOnChunk($chunkCallback);
	}

	/**
	 * 响应解析为数组
	 *
	 * @return array
	 */
	public function toArray()
	{
		return $this->decoded();
	}

	/**
	 * 获取相应内容类型
	 *
	 * @return string
	 */
	public function contentType()
	{
		return $this->getHeaderLine('Content-Type');
	}

	/**
	 * 响应是哪个类型
	 *
	 * @param string|string[] $contentType
	 * @return bool
	 */
	public function isContentType($contentType)
	{
		return Str::is($contentType, $this->contentType());
		// return stripos($this->contentType(), $contentType) !== false;
	}

	/**
	 * 是否为JSON响应
	 *
	 * @return bool
	 */
	public function isJson()
	{
		return $this->isContentType([
			"application/json",
			"application/json;*",
		]);
	}

	/**
	 * 是否为XML响应
	 *
	 * @return bool
	 */
	public function isXml()
	{
		return $this->isContentType([
			"application/xml",
			"application/xml;*",
		]);
	}

	/**
	 * 是否为SSE响应
	 *
	 * @return bool
	 */
	public function isSSE()
	{
		return $this->isContentType([
			"text/event-stream*",
			"application/x-ndjson*",
		]);
	}

	/**
	 * 是否为音频响应
	 *
	 * @return bool
	 */
	public function isAudio()
	{
		return $this->isContentType("audio/*");
	}

	/**
	 * 是否为视频响应
	 *
	 * @return bool
	 */
	public function isVideo()
	{
		return $this->isContentType("video/*");
	}

	/**
	 * 是否为下载流
	 *
	 * @return bool
	 */
	public function isStream()
	{
		if ($this->isAudio() || $this->isVideo()) {
			return true;
		}

		// 检查 Content-Type
		$contentType = $this->contentType();
		$streamContentTypes = [
			'application/octet-stream*',
			'application/zip*',
			'application/x-zip-compressed*',
			'application/pdf*',
			'application/msword*',
			'application/vnd.openxmlformats-officedocument.wordprocessingml.document*',
			'application/vnd.ms-excel*',
			'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet*',
			'application/vnd.ms-powerpoint*',
			'application/vnd.openxmlformats-officedocument.presentationml.presentation*',
		];
		if (in_array($contentType, $streamContentTypes)) {
			return true;
		}

		// 检查 Content-Disposition
		$contentDisposition = $this->getHeaderLine('Content-Disposition');
		if (strpos($contentDisposition, 'attachment') !== false || strpos($contentDisposition, 'inline') !== false) {
			return true;
		}

		return false;
	}

	/**
	 * 将响应保存到文件。
	 *
	 * @param string $filename
	 * @return bool|int
	 */
	public function store($filename)
	{
		return File::put($filename, $this->getBody());
	}

	/**
	 * 将解码数据保存到文件。
	 *
	 * @param string $filename
	 * @return bool|int
	 */
	public function storeAsData($filename)
	{
		return File::put($filename, $this->data());
	}

	/**
	 * 从响应中获取标头。
	 *
	 * @param string $header
	 * @return string
	 */
	public function header(string $header)
	{
		return $this->response->getHeaderLine($header);
	}

	/**
	 * 从响应中获取标头。
	 *
	 * @return array
	 */
	public function headers()
	{
		return $this->response->getHeaders();
	}

	/**
	 * 获取响应的有效 URI。
	 *
	 * @return string|null
	 */
	public function url()
	{
		if ($uri = $this->uri()) {
			return $uri->__toString();
		}

		return $this->requestOptions->get('url');
	}

	/**
	 * 获取响应的有效 URI。
	 * @return UriInterface|null
	 */
	public function uri()
	{
		if ($this->transferStats) {
			return $this->transferStats->getEffectiveUri();
		}

		return null;
	}

	/**
	 * 获取响应的状态代码。
	 *
	 * @return int
	 */
	public function status()
	{
		return $this->response ? (int)$this->response->getStatusCode() : -1;
	}

	/**
	 * 确定请求是否成功。
	 *
	 * @return bool
	 */
	public function successful()
	{
		return $this->status() >= 200 && $this->status() < 300;
	}

	/**
	 * 确定响应代码是否为 “OK”。
	 *
	 * @return bool
	 */
	public function ok()
	{
		return $this->status() === 200;
	}

	/**
	 * 确定响应是否为重定向。
	 *
	 * @return bool
	 */
	public function redirect()
	{
		return $this->status() >= 300 && $this->status() < 400;
	}

	/**
	 * 确定响应是否指示发生了客户端或服务器错误。
	 *
	 * @return bool
	 */
	public function failed()
	{
		return $this->exception || $this->isHttpError();
	}

	/**
	 * 确定响应是否指示发生了客户端或服务器错误。
	 * @return bool
	 */
	public function isHttpError()
	{
		return $this->isServerError() || $this->isClientError();
	}

	/**
	 * 确定响应是否指示发生了客户端错误。
	 *
	 * @return bool
	 */
	public function isClientError()
	{
		return $this->status() >= 400 && $this->status() < 500;
	}

	/**
	 * 确定响应是否指示发生了服务器错误。
	 *
	 * @return bool
	 */
	public function isServerError()
	{
		return $this->status() >= 500;
	}

	/**
	 * 如果存在服务器或客户端错误，则执行给定的回调。
	 *
	 * @param callable $callback
	 * @return $this
	 */
	public function onError(callable $callback)
	{
		if ($this->failed()) {
			$callback($this);
		}

		return $this;
	}

	/**
	 * 获取响应 Cookie。
	 *
	 * @return array
	 */
	public function cookies()
	{
		return $this->httpClient->cookies();
	}

	/**
	 * 获取响应的原因短语。
	 *
	 * @return string
	 */
	public function reason()
	{
		return $this->response->getReasonPhrase();
	}

	/**
	 * 关闭流和任何底层资源。
	 *
	 * @return $this
	 */
	public function close()
	{
		$this->response->getBody()->close();

		return $this;
	}

	/**
	 * @return TransferStats|null
	 */
	public function transferStats()
	{
		return $this->transferStats;
	}

	/**
	 * 获取响应的基础请求。
	 *
	 * @return RequestInterface
	 */
	public function request()
	{
		return $this->request;
	}

	/**
	 * 获取响应的基础 PSR 响应。
	 *
	 * @return ResponseInterface
	 */
	public function toPsrResponse()
	{
		return $this->response;
	}

	/**
	 * 如果发生 server 或 client 错误，则创建例外。
	 *
	 * @return HttpClientException
	 */
	public function toException()
	{
		if ($this->exception) {
			if ($this->exception instanceof BadResponseException) {
				return new ResponseException(
					$this,
					$this->exception
				);
			} else {
				return new RequestException(
					$this->exception->getMessage(),
					$this->exception->getCode(),
					$this->exception
				);
			}
		}

		if ($this->failed()) {
			return new ResponseException($this);
		}

		return null;
	}

	/**
	 * @return HttpClient
	 */
	public function httpClient()
	{
		return $this->httpClient;
	}

	/**
	 * @return Fluent
	 */
	public function requestOptions()
	{
		return $this->requestOptions;
	}

	/**
	 * 存在错误则引发异常
	 *
	 * @param \Closure|null $callback
	 * @return $this
	 * @noinspection PhpDocMissingThrowsInspection
	 */
	public function throw(\Closure $callback = null)
	{
		if ($e = $this->toException()) {
			throw tap($e, function ($e) use ($callback) {
				if ($callback && is_callable($callback)) {
					$callback($this, $e);
				}
			});
		}

		return $this;
	}

	/**
	 * 给定条件的计算结果为 true，并且存在错误则引发异常。
	 *
	 * @param bool $condition
	 * @return $this
	 * @noinspection PhpDocMissingThrowsInspection
	 */
	public function throwIf($condition)
	{
		return $condition ? $this->throw() : $this;
	}

	/**
	 * 除非是HTTP错误才不抛出异常。
	 *
	 * @return $this
	 * @noinspection PhpDocMissingThrowsInspection
	 */
	public function throwUnlessHttpError()
	{
		if (!$this->isHttpError()) {
			$this->throw();
		}

		return $this;
	}

	/**
	 * 确定给定的偏移量是否存在。
	 *
	 * @param string $offset
	 * @return bool
	 */
	#[\ReturnTypeWillChange]
	public function offsetExists($offset)
	{
		$this->decoded();
		return isset($this->decoded[$offset]);
	}

	/**
	 * 获取响应结果指定的值
	 *
	 * @param string $offset
	 * @return mixed
	 */
	#[\ReturnTypeWillChange]
	public function offsetGet($offset)
	{
		$this->decoded();
		return $this->decoded[$offset];
	}

	/**
	 * 不允许重新设定值
	 *
	 * @param string $offset
	 * @param mixed $value
	 * @return void
	 *
	 * @throws \LogicException
	 */
	#[\ReturnTypeWillChange]
	public function offsetSet($offset, $value)
	{
		$this->decoded();
		$this->decoded[$offset] = $value;
		// throw new LogicException('Response data may not be mutated using array access.');
	}

	/**
	 * 取消设置给定的值。
	 *
	 * @param string $offset
	 * @return void
	 *
	 * @throws \LogicException
	 */
	#[\ReturnTypeWillChange]
	public function offsetUnset($offset)
	{
		$this->decoded();
		unset($this->decoded[$offset]);
		// throw new LogicException('Response data may not be mutated using array access.');
	}

	/**
	 * 获取响应的正文。
	 *
	 * @return string
	 */
	public function __toString()
	{
		return $this->body();
	}

	/**
	 * 将其他方法动态代理到基础响应。
	 *
	 * @param string $method
	 * @param array $parameters
	 * @return mixed
	 */
	public function __call($method, $parameters)
	{
		return static::hasMacro($method)
			? $this->macroCall($method, $parameters)
			: $this->response->{$method}(...$parameters);
	}

	/**
	 * @return array|null
	 */
	public function __debugInfo()
	{
		return [
			'decoded' => $this->decoded,
			'response' => $this->response,
			'request' => $this->request,
			// 'transferStats' => $this->transferStats,
			// 'requestOptions' => $this->requestOptions,
			'exception' => $this->exception,
		];
	}

}
